﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using OverStore.Exceptions;
using System.Globalization;
using OverStore.Runtime.ObjectRepository;
using OverStore.Runtime.ObjectRepository.Depot.IdentifierGenerator;
using OverStore.Runtime.ObjectRepository.Accessors;

namespace OverStore.Runtime.ObjectRepository.Depot
{
    /// <summary>
    /// Implementation of <see cref="IDepot{TInstance, TIdentifier}"/>.
    /// </summary>
    /// <typeparam name="TInstance">The type of the instance.</typeparam>
    /// <typeparam name="TIdentifier">The type of the identifier.</typeparam>
    public sealed class GenericDepot<TInstance, TIdentifier> : IDepot<TInstance, TIdentifier>
        where TInstance : class
    {
        private readonly IIdentifierGenerator<TInstance, TIdentifier> _IdentifierGenerator;
        private readonly ITypedAccessor<TInstance, TIdentifier> _StoredIdentifierAccessor;
        private readonly Func<TInstance> _CreateInstance;
        private readonly Dictionary<TInstance, TIdentifier> _KeyMap = new Dictionary<TInstance, TIdentifier>(ReferenceEqualityComparer<TInstance>.Instance);
        private readonly Dictionary<TIdentifier, TInstance> _IdentityMap;
        private readonly Dictionary<TInstance, TIdentifier> _OldIdentifiersMap = new Dictionary<TInstance, TIdentifier>(ReferenceEqualityComparer<TInstance>.Instance);

        /// <summary>
        /// Initializes a new instance of the <see cref="GenericDepot{TInstance, TIdentifier}"/> class.
        /// </summary>
        /// <param name="createInstance">Delegate that creates new instance of <typeparamref name="TInstance"/> type.</param>
        /// <param name="identifierEqualityComparer">The equality comparer uses for compare identifiers.</param>
        /// <param name="storedIdentifierAccessor">The stored identifier accessor.</param>
        /// <param name="identifierGenerator">The identifier generator.</param>
        public GenericDepot(
            Func<TInstance> createInstance,
            IEqualityComparer<TIdentifier> identifierEqualityComparer,
            ITypedAccessor<TInstance, TIdentifier> storedIdentifierAccessor,
            IIdentifierGenerator<TInstance, TIdentifier> identifierGenerator)
        {
            if (createInstance == null)
                throw new ArgumentNullException("createInstance");
            _CreateInstance = createInstance;

            if (identifierEqualityComparer == null)
                throw new ArgumentNullException("identifierEqualityComparer");
            _IdentityMap = new Dictionary<TIdentifier, TInstance>(identifierEqualityComparer);

            _StoredIdentifierAccessor = storedIdentifierAccessor;

            if (identifierGenerator == null)
                throw new ArgumentNullException("identifierGenerator");
            _IdentifierGenerator = identifierGenerator;
        }

        /// <summary>
        /// 	<para> Return identifier of specified instance.</para>
        /// 	<para>If instance does not registered in depot, the instance will be placed in depot,
        /// and new identifier will be assigned to according to configuration rules.</para>
        /// </summary>
        /// <param name="instance">The instance to get identifier from.</param>
        /// <returns>The instance identifier</returns>
        /// <exception cref="PersistingRuntimeException">Error occured while get object identifier.</exception>
        public TIdentifier GetObjectIdentifier(TInstance instance)
        {
            TIdentifier key;
            if (_KeyMap.TryGetValue(instance, out key))
                return key;
            var id = _IdentifierGenerator.NextIdentifier(instance);
            _KeyMap.Add(instance, id);
            _IdentityMap.Add(id, instance);
            return id;
        }

        /// <summary>
        /// Gets the stored identifier for the instance. 
        /// Stored identifier is the copy of instance's depot identifier copied to instance's data.
        /// If there is no identifier's copy in instance data, method must returns <c>default(TIdentifier)</c>.
        /// </summary>
        public TIdentifier GetStoredIdentifier(TInstance instance)
        {
            if (_StoredIdentifierAccessor == null)
                return default(TIdentifier);
            try
            {
                return _StoredIdentifierAccessor.GetValue(instance);
            }
            catch (Exception ex)
            {
                throw new PersistingRuntimeException(
                            String.Format("Depot error: getting stored identifier of instance of type {0} failed.", typeof(TInstance)),
                            ex);
            }
        }

        /// <summary>
        /// Sets the stored identifier to the instance.
        /// </summary>
        public void SetStoredIdentifier(TInstance instance, TIdentifier oldIdentifier)
        {
            if (_StoredIdentifierAccessor != null)
            {
                try
                {
                    _StoredIdentifierAccessor.SetValue(instance, oldIdentifier);
                }
                catch (Exception ex)
                {
                    throw new PersistingRuntimeException(
                            String.Format("Depot error: setting stored identifier of instance of type {0} failed.", typeof(TInstance)),
                            ex);
                }
            }
        }

        /// <summary>
        /// Update identifier of instance registered in session. After update the instance will no
        /// longer accessible by old identifier.
        /// If instance does not registered in session, it will be placed in
        /// session cache with specified identifier.
        /// </summary>
        /// <param name="instance">The instance.</param>
        /// <param name="newIdentifier">The new identifier.</param>
        /// <exception cref="PersistingRuntimeException">Error occured while update object identifier.</exception>
        /// <exception cref="IdentityReferenceIntegrityException">Different instance with same key already registered.</exception>
        public void PutNewIdentifier(TInstance instance, TIdentifier newIdentifier)
        {
            TIdentifier oldId;
            if (!_KeyMap.TryGetValue(instance, out oldId))
                throw new PersistingRuntimeException("Putting new identifier for entity not registered in depot.");
            _OldIdentifiersMap[instance] = oldId;

            _KeyMap[instance] = newIdentifier;
            _IdentityMap.Remove(oldId);
            _IdentityMap.Add(newIdentifier, instance);
        }

        /// <summary>
        /// Removes identifier for the instance that was put by calling PutNewIdentifier method.
        /// This method has effect only before AcceptNewIdentifier method call.
        /// </summary>
        public void RejectNewIdentifier(TInstance instance)
        {
            TIdentifier oldIdentifier;
            if (_OldIdentifiersMap.TryGetValue(instance, out oldIdentifier))
            {
                TIdentifier newId;
                if (!_KeyMap.TryGetValue(instance, out newId))
                    throw new PersistingRuntimeException("Rejecting new identifier for entity not registered in depot.");

                _IdentityMap.Remove(newId);
                _IdentityMap.Add(oldIdentifier, instance);

                _KeyMap[instance] = oldIdentifier;

                SetStoredIdentifier(instance, oldIdentifier);
            }
        }

        /// <summary>
        /// Replaces the identifier of the instance registered in session with the
        /// new identifier that was put before by calling PutNewIdentifier method.
        /// After accepting new identifier instance become available to get by new identifier,
        /// and old identifier discards and becomes invalid.
        /// </summary>
        /// <param name="instance">The instance.</param>
        public void AcceptNewIdentifier(TInstance instance)
        {
            TIdentifier newIdentifier;
            if (!_KeyMap.TryGetValue(instance, out newIdentifier))
                throw new PersistingRuntimeException("Accepting identifier of entity not registered in session.");

            SetStoredIdentifier(instance, newIdentifier);
            _OldIdentifiersMap.Remove(instance);
        }

        /// <summary>
        /// Put instance in depot if instance not exist.
        /// If instance already exist, the false returns.
        /// The true is returned, if instance not exist previously.
        /// </summary>
        /// <param name="instance">The instance to put in depot.</param>
        /// <returns>
        ///     <c>true</c> if instance successfuly put into depot, false if instance with such identifier already exists.
        /// </returns>
        /// <exception cref="PersistingRuntimeException">Error is occured while put instance.</exception>
        /// <exception cref="IdentityReferenceIntegrityException">Different instance with same key already registered.</exception>
        public bool TryPutInstance(TInstance instance)
        {
            if (_KeyMap.ContainsKey(instance))
                return false;
            TIdentifier id = _IdentifierGenerator.NextIdentifier(instance);
            if (_IdentityMap.ContainsKey(id))
            {
                var existing = _IdentityMap[id];
                throw new IdentityReferenceIntegrityException(existing, instance);
            }

            SetStoredIdentifier(instance, id);

            _KeyMap.Add(instance, id);
            _IdentityMap.Add(id, instance);

            return true;
        }

        /// <summary>
        /// Get object from depot using identifier. If object
        /// with such identifier not exist in depot, the new instance
        /// creates and puts into depot.
        /// The creates out parameter is set to true if object was created,
        /// and set to false, if object already exist.
        /// </summary>
        /// <param name="identifier">The identifier to get or create object with it.</param>
        /// <param name="created">if set to <c>true</c> it means object was created; otherwise method returns instance already registered in depot.</param>
        /// <exception cref="PersistingRuntimeException">Error is occured while get or create object.</exception>
        public TInstance GetOrCreate(TIdentifier identifier, out bool created)
        {
            TInstance instance;
            created = !_IdentityMap.TryGetValue(identifier, out instance);
            if (created)
                return CreateInstanceWithIdentifier(identifier);
            return instance;
        }

        /// <summary>
        /// Remove instance from depot and removes pending new identifier for the instance.
        /// </summary>
        /// <param name="instance">The instance to remove from depot.</param>
        public void RemoveObject(TInstance instance)
        {
            TIdentifier id;
            if (_KeyMap.TryGetValue(instance, out id))
                _IdentityMap.Remove(id);
            _KeyMap.Remove(instance);
            _OldIdentifiersMap.Remove(instance);
        }

        /// <summary>
        /// Get all object created with depot or put into depot.
        /// </summary>
        /// <returns>
        /// Iterator throught set of registered objects.
        /// </returns>
        public IEnumerable<TInstance> RegisteredObjects()
        {
            return _IdentityMap.Values.ToList();
        }

        /// <summary>
        /// Determines whether the specified instance is registered in depot.
        /// </summary>
        /// <param name="instance">The instance to determine whether is it registered.</param>
        /// <returns>
        ///     <c>true</c> if the instance registered in depot; otherwise, <c>false</c>.
        /// </returns>
        public bool IsInstanceRegistered(TInstance instance)
        {
            Debug.Assert(instance != null, "Check null instance whether it is registered.");
            return _KeyMap.ContainsKey(instance);
        }

        private TInstance CreateInstanceWithIdentifier(TIdentifier identifier)
        {
            TInstance instance = null;
            try
            {
                instance = _CreateInstance();
            }
            catch (Exception ex)
            {
                throw new PersistingRuntimeException(
                            String.Format("Depot error: creating instance of type {0} failed.", typeof(TInstance)),
                            ex);
            }

            SetStoredIdentifier(instance, identifier);

            _IdentityMap.Add(identifier, instance);
            _KeyMap.Add(instance, identifier);
            return instance;
        }
    }
}